﻿
Imports System.Net
Imports System.IO
Imports System.IO.Compression

Public Class httpproxy2
    Public Const defaultRetryTimes As Int64 = 4
    Public Const defaultHoldtime As Int64 = 30000
    Public Const defaultSleeptime As Int64 = 100
    Public Const MIN_GZIP_SIZE As Int64 = 1024
    Private Shared ReadOnly HTTPPROXY2_REQUEST_RESPOND_TIMEMS As Int64

    Protected context As httpContext = Nothing
    Protected request As HttpWebRequest = Nothing
    Protected response As HttpWebResponse = Nothing
    Private out_match As String = Nothing   'start from /
    Private in_match As String = Nothing    'full url
    Private in_requestPathquery As String = Nothing
    Private ex As Exception = Nothing

    Private holdtime As Int64

    Shared Sub New()
        HTTPPROXY2_REQUEST_RESPOND_TIMEMS =
            counter.registerAverageAndLastAverage("HTTPPROXY2_REQUEST_RESPOND_TIMEMS")
    End Sub

    Public Sub initial(Optional ByVal holdtime As Int64 = defaultHoldtime)
        Me.holdtime = holdtime
    End Sub

    Public Sub release()
        If Not context Is Nothing AndAlso Not context.closed() Then
            context.close()
        End If
        If Not response Is Nothing Then
            closestream(response.GetResponseStream())
            response.Close()
        End If
        context = Nothing
        request = Nothing
        response = Nothing
    End Sub

    Public Shared Sub release(ByVal i As httpproxy2)
        If Not i Is Nothing Then
            i.release()
        End If
    End Sub

    Public Function exception() As Exception
        Return ex
    End Function

    Public Function proxy(ByVal ctx As httpContext, Optional ByRef action As callbackAction = Nothing) As Boolean
        If ctx Is Nothing Then
            Return False
        Else
            Return proxy(ctx, ctx.url().pathquery, ctx.url().full, action)
        End If
    End Function

    Private Function holdticks() As Int64
        Return millisecondsToTicks(holdtime)
    End Function

    Private Function getResponse(ByRef action As callbackAction) As Boolean
        Dim startticks As Int64 = 0
        action = New callbackActionAsyncOperation(Function(ByRef ac As AsyncCallback,
                                                           ByRef obj As Object) As IAsyncResult
                                                      startticks = Now.Ticks()
                                                      Return request.BeginGetResponse(ac, obj)
                                                  End Function,
                                                  Sub(ByRef ar As IAsyncResult)
                                                      counter.increase(HTTPPROXY2_REQUEST_RESPOND_TIMEMS,
                                                                       ticksToMilliseconds(Now.Ticks() - startticks))
                                                      Try
                                                          response = request.EndGetResponse(ar)
                                                      Catch ex As WebException
                                                          response = convertor.cast(Of HttpWebResponse) _
                                                                                   (ex.Response(), False)
                                                          If response Is Nothing Then
                                                              copy(Me.ex, ex)
                                                          End If
                                                      Catch ex As Exception
                                                          copy(Me.ex, ex)
                                                      End Try
                                                  End Sub,
                                                  Function() As Boolean
                                                      Return Not response Is Nothing
                                                  End Function,
                                                  holdticks())
        Return begin(action)
    End Function

    Public Function proxy(ByVal ctx As httpContext, ByVal out_match As String, ByVal in_match As String,
                          Optional ByRef action As callbackAction = Nothing) As Boolean
        context = ctx
        copy(Me.out_match, out_match)
        copy(Me.in_match, in_match)

        in_requestPathquery = strmid(context.url().pathquery, strlen(out_match))
        request = Nothing
        response = Nothing
        ex = Nothing

        Dim act As callbackAction = Nothing
        Const s_syncRequestBody As UInt32 = stepCallbackAction.initial_step
        Const s_getResponse As UInt32 = s_syncRequestBody + 1
        Const s_postResponse As UInt32 = s_getResponse + 1
        action = New stepCallbackAction(Function(ByRef s As UInt32) As Boolean
                                            Select Case s
                                                Case s_syncRequestBody
                                                    Return create() AndAlso syncRequestBody(act)
                                                Case s_getResponse
                                                    Return getResponse(act)
                                                Case s_postResponse
                                                    Return postresponse(act)
                                                Case Else
                                                    Return assert(False)
                                            End Select
                                        End Function,
                                        Function(ByRef s As UInt32) As Boolean
                                            Return stepCallbackAction.action_check(
                                                        act,
                                                        Sub(ByRef _step As UInt32)
                                                            _step += 1
                                                            If _step > s_postResponse Then
                                                                _step = stepCallbackAction.finished_step
                                                            End If
                                                        End Sub,
                                                        s)
                                        End Function,
                                        Function() As Boolean
                                            If Not response Is Nothing Then
                                                response.Close()
                                            End If
                                            Return callbackAction.success_finished(act)
                                        End Function,
                                        holdticks())
        Return begin(action)
    End Function

    Protected Overridable Function create() As Boolean
        If debugassert(Not context Is Nothing, "do not have a valid httpContext input.") Then
            Try
                request = HttpWebRequest.Create(in_match + in_requestPathquery)
            Catch
                raiseError("cannot create HttpWebRequest, " + in_match + in_requestPathquery, _
                           errorHandle.errorType.exclamation)
                Return False
            End Try

            request.Headers().Clear()
            request.Method = context.request().HttpMethod
            copyHttpRequestHeaders(context.request(), request)
            'to see whether webrequest can use the same tcp connection in different instances.
            'request.KeepAlive = True
            'request.Connection = network.http.headers.connection.keep_alive
            'do not allow auto redirect, each http server will handle it, and FD deals it with following logic
            request.AllowAutoRedirect = False
            Return True
        Else
            Return False
        End If
    End Function

    Private Function syncRequestBody(ByRef action As callbackAction) As Boolean
        If debugassert(Not context Is Nothing AndAlso Not request Is Nothing, _
                       "do not have a valid httpContext / HttpWebRequest input.") Then
            If context.request().HasEntityBody() Then
                Dim requestStream As Stream = Nothing
                Dim getAction As callbackAction = Nothing
                getAction = New callbackActionAsyncOperation(Function(ByRef ac As AsyncCallback,
                                                                      ByRef obj As Object) As IAsyncResult
                                                                 Return request.BeginGetRequestStream(ac, obj)
                                                             End Function,
                                                             Sub(ByRef ar As IAsyncResult)
                                                                 Try
                                                                     requestStream = request.EndGetRequestStream(ar)
                                                                 Catch ex As Exception
                                                                     copy(Me.ex, ex)
                                                                 End Try
                                                             End Sub,
                                                             Function() As Boolean
                                                                 Return Not requestStream Is Nothing
                                                             End Function,
                                                             holdticks())
                Dim syncAction As callbackAction = Nothing
                Dim synclength As pointer(Of Int64) = New pointer(Of Int64)(0)
                action = New callbackAction(Function() As Boolean
                                                Return begin(getAction)
                                            End Function,
                                            callbackAction.step_check(
                                                callbackAction.action_sub_check(
                                                    getAction,
                                                    Function() As Boolean
                                                        Return syncstream3(context.request.InputStream,
                                                                           requestStream,
                                                                           length:=context.request().ContentLength64,
                                                                           holdtime:=holdtime,
                                                                           action:=syncAction,
                                                                           result:=synclength)
                                                    End Function),
                                                callbackAction.action_sub_check(
                                                    Function() As callbackAction
                                                        Return syncAction
                                                    End Function)),
                                            Function() As Boolean
                                                closestream(requestStream)
                                                Return (+synclength) = context.request().ContentLength64()
                                            End Function,
                                            holdticks())
            Else
                action = callbackAction.finished_success()
            End If
            Return begin(action)
        Else
            Return False
        End If
    End Function

    Protected Overridable Sub generateResponseHeaders()
        context.response().Headers.Clear()
        copyHttpResponseHeaders(response, context.response())
        context.setstatuscode(response.StatusCode, response.StatusDescription)
    End Sub

    Private Function redirect(ByRef action As callbackAction)
        action = New callbackAction(end:=Function() As Boolean
                                             If response.ResponseUri.Host = request.RequestUri.Host Then
                                                 Dim responsePart As String = Nothing
                                                 responsePart = strleft(context.url().full,
                                                                        strlen(context.request.Url.ToString) -
                                                                        strlen(in_requestPathquery))
                                                 If strendwith(responsePart, network.pathSeperator) AndAlso
                                                    strstartwith(response.ResponseUri.PathAndQuery,
                                                                 network.pathSeperator) Then
                                                     strrmv(responsePart,
                                                            strlen(responsePart) - strlen(network.pathSeperator))
                                                 End If
                                                 context.response.Redirect(responsePart +
                                                                           response.ResponseUri.PathAndQuery)
                                             Else
                                                 context.response.Redirect(response.ResponseUri.AbsoluteUri())
                                             End If
                                             Return True
                                         End Function,
                                    timeoutticks:=holdticks())
        Return begin(action)
    End Function

    Private Shared Function enableGzip(ByVal contentType As String) As Boolean
        Return (strstartwith(contentType, "text", False) OrElse _
                strsame(contentType, network.http.contentType.applicationOctetStream) OrElse _
                strsame(contentType, network.http.contentType.rssxml))
    End Function

    Protected Overridable Function enableGzip() As Boolean
        Return context.acceptGzip() AndAlso _
               (Not contentLengthValid(response) OrElse response.ContentLength > MIN_GZIP_SIZE) AndAlso _
               enableGzip(response.ContentType)
    End Function

    Private Function syncResponseBody(ByRef action As callbackAction) As Boolean
        Dim s As Stream = Nothing
        Try
            s = response.GetResponseStream()
        Catch
        End Try
        If Not s Is Nothing Then
            Dim dest As Stream = Nothing
            Dim gzipStream As Stream = Nothing
            If enableGzip() Then
                context.buff().Position() = 0
                context.buff().SetLength(0)
                gzipStream = New GZipStream(context.buff(), CompressionMode.Compress, True)
                context.setcontentLength64(0)
                modifyheader(context.response().Headers, network.http.headers.ContentEncoding, network.gzipCompress)
                dest = gzipStream
            ElseIf contentLengthValid(response) Then
                'if contentLength is valid, context.response().ContentLength64() should be set already.
                dest = context.response().OutputStream()
            Else
                dest = context.buff()
            End If
            Dim syncAction As callbackAction = Nothing
            Dim synclen As pointer(Of Int64) = New pointer(Of Int64)(0)
            action = New callbackAction(Function() As Boolean
                                            Return syncstream3(s, dest,
                                                               length:=ifoper(contentLengthValid(response),
                                                                              response.ContentLength,
                                                                              npos),
                                                               holdtime:=holdtime,
                                                               action:=syncAction,
                                                               result:=synclen)
                                        End Function,
                                        callbackAction.action_check(
                                            Function() As callbackAction
                                                Return syncAction
                                            End Function),
                                        Function() As Boolean
                                            If contentLengthValid(response) AndAlso _
                                               (+synclen) <> response.ContentLength Then
                                                raiseError("cannot syncstream to request " + context.url.full, _
                                                           errorHandle.errorType.warning)
                                                'still return true here, as the http context is out of usage now.
                                            End If
                                            closestream(gzipStream)
                                            Return True
                                        End Function,
                                        holdticks())
            Return begin(action)
        Else
            Return False
        End If
    End Function

    Private Function postresponse(ByRef action As callbackAction) As Boolean
        If debugassert(Not context Is Nothing AndAlso Not request Is Nothing AndAlso Not response Is Nothing, _
                       "do not have a valid httpContext or HttpWebRequest or HttpWebResponse input.") Then
            If response.ResponseUri <> request.RequestUri Then
                Return redirect(action)
            Else
                generateResponseHeaders()
                Return syncResponseBody(action)
            End If
        Else
            Return False
        End If
    End Function
End Class

Public Class httpproxy2Pool_t
    Inherits pool3(Of httpproxy2)

    Protected Overrides Function create() As httpproxy2
        Return New httpproxy2()
    End Function

    Protected Overrides Sub [erase](ByRef inst As httpproxy2)
        httpproxy2.release(inst)
        MyBase.erase(inst)
    End Sub

    Protected Overrides Sub free(ByVal inst As httpproxy2)
        httpproxy2.release(inst)
        MyBase.free(inst)
    End Sub
End Class
